home *** CD-ROM | disk | FTP | other *** search
/ NetNews Offline 2 / NetNews Offline Volume 2.iso / news / comp / lang / c++-part1 / 5394 < prev    next >
Encoding:
Text File  |  1996-08-05  |  20.2 KB  |  482 lines

  1. Path: news1.halcyon.com!coho!riston
  2. From: riston@coho.halcyon.com (Michael D. Riston)
  3. Newsgroups: comp.lang.c++
  4. Subject: SUMMARY: A Good UNDO Design
  5. Date: 4 Feb 1996 08:55:36 GMT
  6. Organization: Northwest Nexus Inc.
  7. Message-ID: <4f1sa8$kd7@news1.halcyon.com>
  8. NNTP-Posting-Host: coho.halcyon.com
  9.  
  10.  
  11. Hello Designers!
  12.  
  13. This summary contains the responses received for a question posted on the 
  14. net asking for any ideas on an elegant object oriented design for 
  15. implementing Undo/Redo.  Hope this helps. 
  16.  
  17. Mike Riston 
  18. E-Mail - riston@halcyon.com 
  19. Web    - http://www.halcyon.com/riston 
  20.  
  21. The original question
  22. ---------------------
  23.  
  24.    I have been charged with designing an undo mechanism for a large 
  25.    graphical Windows (32bit) application.  This application is the classic 
  26.    'draw a shape' program (box, circle, line, OLE object, or whatever).   
  27.    I would like to know if anybody has any elegant designs for handling 
  28.    undo operations.  I am working with Visual C++ 4.0 and MFC for the 
  29.    implementation.  I am well versed in C++.
  30.  
  31.  
  32. My own comments 
  33. ---------------
  34.  
  35.    Thank you to all who submitted answers to this question!  The most
  36.    common answer was the 'Command' pattern found in the 'Design Patterns'
  37.    book, by Gamma, Helm, Johnson, and Vlissides.  By far the benefits of 
  38.    using this design make this a classic answer for the Undo/Redo design.  
  39.    This is the method that our design will take.  Many people pointed to 
  40.    Jim Beveride's Undo/Redo article in the Feb 1996 issue of Dr. Dobbs as 
  41.    an Undo/Redo example.  His design basically implements the 'Command' 
  42.    pattern, with a fancy Undo/Redo command list manager class that he 
  43.    calls CForeman.  The summary reads.  
  44.    
  45.       "Implementing Multilevel UNDO/REDO by Jim Beveridge. The Undo/Redo 
  46.       mechanism Jim presents here is based on a history length limited 
  47.       only by available memory.  Because it is implemented in Visual C++ 
  48.       and MFC, this machanism can easily be added to your applications." 
  49.       
  50.    Below is a selected list of answers received from netland.  Most seem 
  51.    to use the 'Command' pattern with different twists and turns.  
  52.    
  53.  
  54.    ----------------------------------------------------------------------
  55.    enno@inferenzsysteme.informatik.th-darmstadt.de (Enno Sandner) Writes
  56.    ----------------------------------------------------------------------
  57.    
  58.    A neat solution to this problem is the 'Command' pattern as described in
  59.    the 'Design Patterns' book. The main idea is to encapsulate the necessary
  60.    steps to perform a specific behavior in objects that conform to an unique
  61.    interface. The interface provides the services 'Do' and 'Undo'. It is
  62.    represented by the abstract 'Command' class:
  63.  
  64.         class Command {
  65.           public:
  66.             virtual ~Command() {}
  67.             virtual void Do()=0;
  68.                     virtual void Undo()=0;
  69.                 };
  70.  
  71.    Every operation that affects the graphical appearence must have its counter-
  72.    part that reverses the operation and both operations should be encapsulated
  73.    in a subclass of 'Command'. For example a circle may look like:
  74.  
  75.         class CircleCommand : public Command {
  76.           public:
  77.            CircleCommand(Painter& p,int x,int y,int r) : 
  78.             p_(p), x_(x), y_(y), r_(r) {}
  79.            void Do() { p_.DrawCircle(x_,y_,r_); }
  80.            void Undo() { p_.RemoveCircle(x_,y_,r_); }
  81.         };
  82.  
  83.    Moving a graphical object is another example for an operation that should
  84.    be wrapped.  Finally you need a manager to register the commands. The 
  85.    manager maintains a finite command history and allows you to move backwards 
  86.    and forwards in the history. The former results in undoing the latter in 
  87.    executing the command.
  88.  
  89.     Enno
  90.  
  91.  
  92.  
  93.    --------------------------------------
  94.    rdwells@mmm.com (Richard Wells) Writes
  95.    --------------------------------------
  96.  
  97.    Here's a technique I've seen used:
  98.  
  99.    class Command
  100.    {
  101.        virtual void Do();
  102.        virtual void Undo();
  103.        virtual void Redo();
  104.        virtual void Commit();
  105.    };
  106.  
  107.    Each individual command is represented by a class derived from Command,
  108.    with implementations for these 4 functions.  The past n commands (where
  109.    n is the size of the undo buffer) are kept in a queue of sorts; it isn't
  110.    quite a strict queue, as we'll see, but calling it a queue will suffice
  111.    for now.
  112.  
  113.    The Do() function is called when the command is first issued.  This
  114.    does everything necessary to make it appear to the user that the command
  115.    has been carried out, except that it does not permanently affect the
  116.    underlying data.  It also must keep around whatever data is necessary
  117.    to undo the command.
  118.  
  119.    The Undo() function is called when the command is undone.  It must make
  120.    it appear to the user that the command has been undone.  Like Do(), it
  121.    makes no permanent changes to the underlying data.
  122.  
  123.    The Redo() function is called when the command is being redone.  It is
  124.    generally very similar to Do(); it does whatever it takes to make it
  125.    appear to the user that the command has been redone, without permanently
  126.    changing the underlying data.
  127.  
  128.    Finally, Commit() is called to make the necessary changes to the underlying
  129.    data to make an undo impossible.
  130.  
  131.    Now for the undo buffer: this is actually a queue (sort of) of command
  132.    instances.  New commands are added to the head, and old commands are
  133.    removed as necessary from the tail.  However, this is not a strict queue,
  134.    because we also need to keep a pointer to the middle of the queue; for
  135.    reasons that will become obvious, we'll call this the stack pointer.
  136.  
  137.    When the user selects "undo", the Undo() function of the command pointed
  138.    to by the stack pointer is called, and the stack pointer is modified to
  139.    point to the previous Command.  Note that the Command object whose Undo()
  140.    function was called is NOT removed from the queue.
  141.  
  142.    When the user selects "redo", the stack pointer is modified to point to
  143.    the next Command object, and its Redo() function is called.
  144.  
  145.    To add a new command to the queue, all Command objects between the stack
  146.    pointer and the head of the queue (if any) are removed, the new Command
  147.    object is added at the head of the queue, and its Do() function is called.
  148.    The stack pointer is modified to point to this new object, as it will be
  149.    the first to be undone if the user issues an "undo" command.  If there is
  150.    a limit to the size of the undo buffer (i.e., the number of Command objects
  151.    in the queue), the Commit function for the Command at the tail of the queue
  152.    is called, and that Command object is removed.
  153.  
  154.    Before a "save" command, all Command objects (if any) between the stack
  155.    pointer and the head of the queue are removed, and all remaining objects
  156.    in the queue have their Commit() functions called (starting at the tail
  157.    of the queue); they are then also removed.  (Unless you want to be able to
  158.    Undo() saved operations.  Be warned: there lies madness.)
  159.  
  160.    For example, consider an AddCircle command.  The Do() function would
  161.    draw the circle and store its center and radius.  The Undo() function
  162.    would remove the circle from the screen.  The Redo() function would
  163.    draw the circle again.  The Commit() function would add a circle object
  164.    to the database.
  165.  
  166.    Of course, TANSTAAFL.  A redraw would also need to be able to cruise
  167.    through the undo buffer to redraw all those objects between the tail
  168.    of the queue and the stack pointer; this may or may not be the same
  169.    as the Redo() function, depending on circumstances.  (If you use a
  170.    model where your display list is separate from the database, this may
  171.    not be a concern, but then the Do(), Undo(), and Redo() functions must
  172.    do the right thing with the display list.)  There are probably other
  173.    real-world considerations I'm glossing over, but I think this scheme
  174.    is generally workable.
  175.  
  176.    Note that if all works according to plan, the Undo() function may
  177.    assume that the world is in the same state as it was after the command
  178.    was first issued, and the Redo() function may assume it is in the
  179.    same state as before the command was first issued.  So, for example,
  180.    the Add Circle command need not concern itself with the possibility
  181.    that a subsequent Move Object command moved the circle; if the user
  182.    has issued "undo" commands to the point where the Add Circle is being
  183.    undone, it can assume that the Move Object has also been undone, and
  184.    the circle is exactly where it was after the Add Circle command was
  185.    originally executed.
  186.  
  187.    Now for the final caveat: I've never had a client who thought that
  188.    undo/redo was important enough to pay for the time it takes to
  189.    implement this scheme.  So if you use it, let me know how it works
  190.    out (:-).
  191.  
  192.    Opinions expressed herein are my own and may not represent those of 3M.
  193.  
  194.  
  195.    ------------------------------------------
  196.    Seth Goldstein <seth@mathworks.com> Writes
  197.    ------------------------------------------
  198.  
  199.    One classic design for unlimited undo/redo is to encapsulate
  200.    each user-initiated command in an object, along with enough 
  201.    information to undo the command or redo it if necessary 
  202.    (subclass the commands to add this information, and any 
  203.    additional information such as some text describes the 
  204.    operation).  Each command object supports an Execute and an 
  205.    Unexecute operation.
  206.  
  207.    The undo manager maintains a series of command objects along 
  208.    with an indicator for "the present".
  209.  
  210.    Undo causes the undo manager to call Unexecute on the command 
  211.    prior to "the present", and move the "present indicator" one 
  212.    notch back.
  213.  
  214.    Redo causes the undo manager to call Execute on the command just 
  215.    after "the present", and move the "present indicator" one notch 
  216.    forward.
  217.  
  218.    There are details to work out regarding commands which are 
  219.    expensive or impossible to undo, commands which have no effect, 
  220.    and what to do with the "future" of the command stream when the 
  221.    user issues an undo followed by a new command.
  222.  
  223.    Hope this is a start,
  224.    Seth
  225.  
  226.    P.S.: See Gamma, Helm, Johnson, Vlissides, _Design_Patterns_
  227.  
  228.  
  229.  
  230.  
  231.  
  232.  
  233.    --------------------
  234.    pifus@CAM.ORG Writes
  235.    --------------------
  236.  
  237.    I have no source code, but if you want to see how it should work from a
  238.    users perspective check out the Describe wordprocessor (it's about $50).
  239.    Instead of the usual Undo/Redo menu options, there is a dialog box with 
  240.    a slider bar which allows you to slide back and forth through 
  241.    the complete history of operations.  Two ways I can see of doing this 
  242.    is make every operation reversible which is more complicated, or only 
  243.    keep a copy of the original document and apply the users instructions 
  244.    to it up to the current undo/redo level, which would be simple but slow.
  245.    Either way you need some sort of stack of instructions, perhaps using 
  246.    an internal macro or symbolic language.
  247.  
  248.    Dave
  249.  
  250.  
  251.    ------------------------------------------------------------------------
  252.    jimb@turningpoint.com (Jim Beveride: Author of Dr. Dobbs article) Writes
  253.    ------------------------------------------------------------------------
  254.  
  255.    > >
  256.    > > I wrote an article that is in this month's Dr. Dobbs Journal
  257.    > > about implementing multilevel Undo/Redo in C++ (and MFC).
  258.    > > It essentially uses the scheme mentioned in a previous post.
  259.    > >
  260.    > > See the February "Dr.Dobbs Journal", which is on shelves now
  261.    > 
  262.    > I picked up a copy today and read the article.  It was very good.
  263.    > It very much follows the Command pattern found in the
  264.    > "Design Patterns" book.
  265.    > 
  266.    > One question, however.   Your undo implementation does not span
  267.    > over multiple documents (i.e. it has a separate undo list for each
  268.    > document).  I have seen other applications (namely Visio) implement
  269.    > UNDO across documents.   For example,  a user puts down a circle in
  270.    > one document, then puts down a square in another, and then undoes
  271.    > both operations and it undoes both documents automatically.   Your
  272.    > spiral program (which I download) did not do this.
  273.    > 
  274.    > Do you have any further ideas on how to implement this?  You
  275.    > mentioned in your article that one would need to implement a
  276.    > GetFocusTask, but yet you would not want the user to see this in the
  277.    > undo list.
  278.    > 
  279.    > Thanks for your input on this topic.  
  280.    > 
  281.    > ......Mike....:)
  282.  
  283.    Mike,
  284.  
  285.    I tried Visio, and it does indeed do what you describe.  I would argue
  286.    that this approach is wrong because it is application-centric instead
  287.    of document-centric.  In other words, if I want to work on several
  288.    documents that are completely unrelated, I cannot randomly switch
  289.    between documents (for example, while one is repaginating or printing)
  290.    and then go back to the "context" of my original document.  It is also
  291.    confusing because multiple instances will work differently than a single
  292.    instance with several MDI windows.  And what happens if the user closes
  293.    a document that is in the undo history... Do you throw away changes
  294.    that referred to that document?  What if it was a drag-and-drop
  295.    operation BETWEEN the two documents?  What if the original view
  296.    on the document is gone, but there is another view that could
  297.    be switched back to instead?
  298.  
  299.    Anyway, if you still want to do it, you can implement a global
  300.    undo history by putting the undo history at the mainframe level
  301.    instead of at the document level.  Each undo task would keep
  302.    a pointer to the document and view it referred (although there
  303.    are problems with this, see above)  Undo is then processed by
  304.    the mainframe, which checks the active view (and document)
  305.    and switches it if necessary.
  306.  
  307.  
  308.    Best of luck,
  309.    Jim Beveridge
  310.  
  311.  
  312.  
  313.  
  314.    --------------------------------------------
  315.    carter@beaconware.com (Carter Maslan) Writes
  316.    --------------------------------------------
  317.  
  318.    Has anyone tried implementing Undo using OLE's Compound Files
  319.    TRANSACTED mode with the revert command?  Inside OLE 2
  320.    inferred that it might be an easy way to support Undo.
  321.  
  322.    ------------------------------------------------------
  323.    From: us018032@interramp.com (John B. Williston) Writes
  324.    ------------------------------------------------------
  325.  
  326.    >Has anyone tried implementing Undo using OLE's Compound Files
  327.    >TRANSACTED mode with the revert command?  Inside OLE 2
  328.    >inferred that it might be an easy way to support Undo.
  329.  
  330.    You *really* don't want to do that (grin).  Each open OLE storage eats
  331.    a chunk of memory and 2 or 3 open file handles.  Using transacted mode
  332.    just makes it worse.  My personal preference is to package commands as
  333.    objects that have Do/Undo methods.
  334.  
  335.    John
  336.  
  337.    (Note.  This thread continues in comp.os.ms-windows.programmer.win32.
  338.     for more information on this see this newsgroup).
  339.  
  340.  
  341.  
  342.  
  343.    ----------------------------------------------------------------
  344.    cascio@ici.net (Joseph Cascio, Jr. // Augusta Development) Writes
  345.    ----------------------------------------------------------------
  346.  
  347.    There are two classic approaches to undo: "inverse command" and 
  348.    "checkpoint".  In the "inverse command" approach, whenever you execute 
  349.    a command on the drawing, like "move", you save the fact that you did 
  350.    a "move", along with its arguments on a list.  To undo, you perform 
  351.    the "inverse" operation, which would be a move with the arguments negated, 
  352.    or however you find it easy to do.  This works as long as the functions 
  353.    don't have a lot of side effects.  A side effect would occur, for example, 
  354.    in a directed-graph type of drawing in which objects are "connected" to
  355.    other objects using lines - a schematic or flowchart, for instance.  When
  356.    you move one object, you may indirectly cause movement or changes of many 
  357.    other objects in ways that are difficult to capture because there is no 
  358.    direct "command" corresponding to that operation. 
  359.  
  360.    In such cases, I prefer the "checkpoint" approach, in which you make a 
  361.    copy of each object before you modify it, then go ahead and do whatever 
  362.    operation you want, then save a copy after all operations are done.  The 
  363.    undo record then consists of a two-part record for each operation 
  364.    performed, which has a "before" list of objects, and an "after" list.  
  365.    To undo, you remove the existing copy of the object, and restore the 
  366.    "before" copy.  To "redo", you remove the existing copy, and restore the 
  367.    "after" copy.
  368.  
  369.    This may cost more in space, but has the benefit that it works no matter 
  370.    what operation you perform, and no matter how many objects it touches.  
  371.    Also, you don't have to write anything additional when you add new 
  372.    functions.  You do have to write an object-clone constructor, and a 
  373.    pointer-mapper, for each new object type you add, however.
  374.  
  375.    This is a pretty condensed explanation, and I'm sure I've raised more 
  376.    questions than I've answered, but perhaps I've launched you in some 
  377.    direction.  I've implemented a couple of versions of the "checkpoint" 
  378.    approach, and found it works pretty well.  Most apps you see today use 
  379.    the "inverse" approach, however.  This is simpler for simple applications, 
  380.    but is limited in the kind of operations it can capture.
  381.  
  382.    BTW, my company specialize in the types of applications you describe for a
  383.    living.  Augusta is in the process of delivering the first release of a
  384.    product for building direct-manipulation diagram editors for Visual C++/MFC.
  385.  
  386.    -JC
  387.  
  388.  
  389.  
  390.    --------------------------------------------------
  391.    kcline@spdmail.spd.dsccc.com (Kevin Cline) Writes
  392.    --------------------------------------------------
  393.  
  394.    I feel strongly that programs that have an undo feature should allow
  395.    the user to undo multiple actions one at a time, all the way back to
  396.    the beginning of the edit session (i.e. back to an empty workspace or
  397.    back to the point a file was loaded).  With only a single undo it is
  398.    too easy for a user to do two things, find that the second-from-last
  399.    can't be undone, and then the user has to start over from the last
  400.    save.  Disk and memory are cheap, so there is no reason not to buffer
  401.    up lots of previous actions.
  402.  
  403.    Keep all the previous actions in a list, like this.
  404.  
  405.    -----------------------------
  406.    | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
  407.    -----------------------------
  408.                  ^
  409.  
  410.    There are three possible user actions: edit, undo, redo.
  411.  
  412.    Edit erases all actions to the right of the undo pointer
  413.    adds the new action to the end of the list, and puts the undo
  414.    pointer under the new action.
  415.  
  416.     -------------------------------
  417.    | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
  418.     -------------------------------
  419.                  ^
  420.    Undo undoes the action above the undo pointer and moves the undo pointer
  421.    to the left.
  422.  
  423.    ----------------------------
  424.    | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
  425.    ----------------------------
  426.              ^
  427.    Redo moves the undo pointer to the right and redoes the action above
  428.    the pointer.
  429.  
  430.    If you have undo and redo in a menu, then the entries could change
  431.    to show what will be undone or redone.
  432.  
  433.    GNU Emacs has a different design; instead of redo, it has undo and undo more.
  434.    The edit list records every state change, even those caused by undo.
  435.  
  436.    Initial state:
  437.  
  438.    ----------------
  439.    | 1 | 2 | 3 | 4 |
  440.    ----------------
  441.  
  442.    After undo:
  443.  
  444.     -------------------------------
  445.    | 1 | 2 | 3 | 4 | 5 (undoing 4) |
  446.     -------------------------------
  447.          ^              
  448.  
  449.    After another undo:
  450.  
  451.     ------------------------------------------------------
  452.    | 1 | 2 | 3 | 4 |      5      |           6            |
  453.    |   |   |   |   | (undoing 4) | (undoing 5, redoing 4) |
  454.     ------------------------------------------------------
  455.          ^           
  456.  
  457.    After undo more:
  458.  
  459.     ---------------------------------------------------------
  460.    | 1 | 2 | 3 | 4 |      5      |     6       |     7       |
  461.    |   |   |   |   | (undoing 4) | (redoing 4) | (undoing 4) | 
  462.     ---------------------------------------------------------
  463.          ^             
  464.  
  465.    After another undo more:
  466.  
  467.     -----------------------------------------------------------------------
  468.    | 1 | 2 | 3 | 4 |      5      |     6       |     7       |      8      |
  469.    |   |   |   |   | (undoing 4) | (redoing 4) | (undoing 4) | (undoing 3) |
  470.     -----------------------------------------------------------------------
  471.      ^
  472.  
  473.    -- 
  474.    Kevin Cline
  475.  
  476.  
  477. --
  478. --------------------------------------------------------------------------
  479. Mike and Hilda Riston    Email  -  riston@halycon.com
  480. 21311 52nd Ave SE    Voice  -  (206) 486-5017
  481. Woodinville, WA 98072   Web    -  http://www.halcyon.com/riston
  482.